using System;
using System.Collections.Generic;
using System.Text;
using csammisrun.OscarLib.Utility;

namespace csammisrun.OscarLib
{
    /// <summary>
    /// Handles all incoming and outgoing peer-to-peer messages
    /// </summary>
    /// <remarks>
    /// <para>This manager is primarily responsible for handling the SNAC family 0x0004.
    /// The session's <see cref="IcqManager"/> and any <see cref="DirectIMConnection"/>s process their messages
    /// and dispatch them to this manager for distribution to the client, though the raw packets are
    /// processed elsewhere.</para>
    /// <para>Chat room messages are not handled by this manager.</para>
    /// </remarks>
    public class MessageManager : ISnacFamilyHandler
    {
        /// <summary>
        /// The SNAC family responsible for sending and receiving instant messages
        /// </summary>
        private const int SNAC_ICBM_FAMILY = 0x0004;

        #region SNAC subtype constants

        /// <summary>
        /// ICBM code for an incoming message
        /// </summary>
        private const int ICBM_INCOMING_MESSAGE = 0x0007;

        /// <summary>
        /// ICBM code for sending a warning to a remote client
        /// </summary>
        private const int ICBM_SEND_WARNING = 0x0008;

        /// <summary>
        /// ICBM code for an outgoing message
        /// </summary>
        private const int ICBM_OUTGOING_MESSAGE = 0x0006;

        /// <summary>
        /// ICBM code for a notification of delivered messages
        /// </summary>
        private const int ICBM_MESSAGE_DELIVERED = 0x000C;

        /// <summary>
        /// ICBM code for setting the SNAC family parameters
        /// </summary>
        private const int ICBM_SET_PARAMETERS = 0x0002;

        /// <summary>
        /// ICBM code for a received parameter list
        /// </summary>
        private const int ICBM_PARAMETER_LIST = 0x0005;

        /// <summary>
        /// ICBM code for requesting message parameters
        /// </summary>
        private const int ICBM_REQUEST_PARAMETERS = 0x0004;

        /// <summary>
        /// ICBM code for received typing notifications
        /// </summary>
        private const int ICBM_TYPING_NOTIFICATION = 0x0014;

        /// <summary>
        /// ICBM code for a notification of undeliverable messages
        /// </summary>
        private const int ICBM_UNDELIVERABLE_MESSAGE = 0x000A;

        /// <summary>
        /// ICBM code for the acknowledgement of a previously sent warning
        /// </summary>
        private const int ICBM_WARNING_ACKNOWLEDGED = 0x0009;

        #endregion

        #region TLV type constants

        /// <summary>
        /// The client IP address of a direct connection
        /// </summary>
        private const int DC_CLIENT_IP_ADDRESS = 0x0003;

        /// <summary>
        /// The message to send with a direct connection invitation
        /// </summary>
        private const int DC_MESSAGE = 0x000C;

        /// <summary>
        /// The port of a direct connection
        /// </summary>
        private const int DC_PORT = 0x0005;

        /// <summary>
        /// The compliment of a direct connection's port
        /// </summary>
        private const int DC_PORT_COMPLIMENT = 0x0017;

        /// <summary>
        /// The proxy IP address of a direct connection
        /// </summary>
        private const int DC_PROXY_IP_ADDRESS = 0x0002;

        /// <summary>
        /// The compliment of a direct connection's proxy IP address
        /// </summary>
        private const int DC_PROXY_IP_ADDRESS_COMPLIMENT = 0x0016;

        /// <summary>
        /// The sequence number of a direct connection
        /// </summary>
        private const int DC_SEQUENCE_NUMBER = 0x000A;

        /// <summary>
        /// A flag for a direct connection to use a proxy
        /// </summary>
        private const int DC_USE_PROXY_FLAG = 0x0010;

        /// <summary>
        /// The verified IP address of a direct connection
        /// </summary>
        private const int DC_VERIFIED_IP_ADDRESS = 0x0004;

        #endregion

        private readonly Session parent;

        /// <summary>
        /// Initializes a new MessageManager
        /// </summary>
        internal MessageManager(Session parent)
        {
            this.parent = parent;
            parent.Dispatcher.RegisterSnacFamilyHandler(this, SNAC_ICBM_FAMILY);
        }

        #region Public methods

        /// <summary>
        /// Sends an instant message
        /// </summary>
        /// <param name="screenName">The screenname to receive the IM</param>
        /// <param name="message">The message to send</param>
        /// <exception cref="NotLoggedInException">Thrown when the <see cref="Session"/> is not logged in</exception>
        public Cookie SendMessage(String screenName, String message)
        {
            return SendMessage(screenName, message, MessageFlags.None, null);
        }

        /// <summary>
        /// Sends an instant message
        /// </summary>
        /// <param name="screenName">The screenname to receive the IM</param>
        /// <param name="message">The message to send</param>
        /// <param name="flags">A <see cref="MessageFlags"/> enumeration specifying what
        /// additional information should be sent with the message</param>
        /// <remarks>If delivery confirmation is requested by setting the <see cref="MessageFlags.RequestAcknowledgement"/>
        /// flag, results will be returned in the <see cref="MessageAccepted"/> event.  If the message is not deliverable
        /// for any reason, the <see cref="MessageUndeliverable"/> event will be raised.</remarks>
        /// <exception cref="NotLoggedInException">Thrown when the <see cref="Session"/> is not logged in</exception>
        public Cookie SendMessage(String screenName, String message, MessageFlags flags)
        {
            return SendMessage(screenName, message, flags, null);
        }

        /// <summary>
        /// Sends an instant message
        /// </summary>
        /// <param name="screenName">The screenname to receive the IM</param>
        /// <param name="message">The message to send</param>
        /// <param name="flags">A <see cref="MessageFlags"/> enumeration specifying what
        /// additional information should be sent with the message</param>
        /// <param name="attachments">A list of <see cref="Attachment"/>s to send with the message</param>
        /// <remarks>
        /// <para>If delivery confirmation is requested by setting the <see cref="MessageFlags.RequestAcknowledgement"/>
        /// flag, results will be returned in the <see cref="MessageAccepted"/> event.  If the message is not deliverable
        /// for any reason, the <see cref="MessageUndeliverable"/> event will be raised.</para>
        /// <para>If any attachments are specified in the <paramref name="attachments"/> list, this method will
        /// attempt to open a Direct Connection, a peer-to-peer connection that could expose the local
        /// IP address to the other participant in the conversation.  If a Direct Connection cannot be established,
        /// this method will not attempt to send the message without the attachments.  The <see cref="Session.DirectIMSessionCancelled"/>
        /// event will be raised, and the client should attempt to send the message without attachments manually.</para></remarks>
        /// <exception cref="NotLoggedInException">Thrown when the <see cref="Session"/> is not logged in</exception>
        public Cookie SendMessage(String screenName, String message, MessageFlags flags,
                                  List<Attachment> attachments)
        {
            if (!parent.LoggedIn)
            {
                throw new NotLoggedInException();
            }

            // See if there is an existing Direct Connection for this screen name.
            // If there is, send the message by it.  If there isn't and there are
            // attachments to send along, create a new DC
            DirectIMConnection conn = parent.Connections.GetDirectIMByScreenname(screenName);
            if (conn != null || (attachments != null && attachments.Count > 0))
            {
                // Make sure the connection hasn't been disconnected on the other side
                if (conn != null && conn.Connected == false)
                {
                    // If there are no attachments, it can be sent through the server,
                    // otherwise the DC will attempt reconnection
                    if (attachments == null || attachments.Count == 0)
                    {
                        // No attachments, just send it through the server
                        return SendMessageThroughServer(screenName, message, flags);
                    }
                }

                if (conn == null)
                {
                    conn = parent.Connections.CreateNewDirectIMConnection(DirectConnectionMethod.Direct,
                                                                          DirectConnectRole.Initiator);
                    RequestDirectConnection(conn);
                }

                DirectIM dim = new DirectIM(screenName, conn);
                dim.Cookie = Cookie.CreateCookieForSending();
                dim.Attachments = attachments;
                dim.Message = message;
                dim.IsAutoResponse = (flags & MessageFlags.AutoResponse) != 0;
                conn.SendMessage(dim);

                return dim.Cookie;
            }
            else
            {
                return SendMessageThroughServer(screenName, message, flags);
            }
        }

        /// <summary>
        /// Sends a typing notification
        /// </summary>
        /// <param name="screenName">The screenname to receive the typing notification</param>
        /// <param name="tn">A <see cref="TypingNotification"/> enumeration specifying what
        /// notification to send</param>
        /// <exception cref="NotLoggedInException">Thrown when the <see cref="Session"/> is not logged in</exception>
        public void SendTypingNotification(string screenName, TypingNotification tn)
        {
            if (!parent.LoggedIn)
            {
                throw new NotLoggedInException();
            }

            DirectIMConnection conn = parent.Connections.GetDirectIMByScreenname(screenName);
            if (conn != null)
            {
                if (conn.Connected)
                {
                    // TODO:  send DIM typing notifications
                }
            }

            // Construct SNAC(04,14)
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_TYPING_NOTIFICATION;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            ByteStream stream = new ByteStream();
            stream.WriteByteArray(new byte[] {0, 0, 0, 0, 0, 0, 0, 0});
            stream.WriteUshort(0x0001);
            stream.WriteByte((byte) Encoding.ASCII.GetByteCount(screenName));
            stream.WriteString(screenName, Encoding.ASCII);
            stream.WriteUshort((ushort) tn);

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        /// <summary>
        /// Invites an AIM user to an AOL chatroom
        /// </summary>
        /// <param name="chatroom">The <see cref="ChatRoom"/> describing the chatroom</param>
        /// <param name="screenName">The screenname of the user to invite</param>
        /// <param name="message">A message to send along with the invitation</param>
        public Cookie InviteToChatRoom(ChatRoom chatroom, string screenName, string message)
        {
            if (!parent.LoggedIn)
            {
                throw new NotLoggedInException();
            }

            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_OUTGOING_MESSAGE;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            Cookie cookie = Cookie.CreateCookieForSending();

            Encoding enc = Marshal.ASCII;
            byte destlength = (byte) enc.GetByteCount(screenName);
            ushort messagelength = (ushort) enc.GetByteCount(message);
            byte roomnamelength = (byte) enc.GetByteCount(chatroom.FullName);

            ByteStream stream = new ByteStream();
            InsertIcbmHeader(stream, cookie, 0x0002, screenName);

            ByteStream header = new ByteStream();
            header.WriteUshort(0x0000);
            header.WriteByteArray(cookie.ToByteArray());
            header.WriteUint(0x748F2420);
            header.WriteUint(0x628711D1);
            header.WriteUint(0x82224445);
            header.WriteUint(0x53540000);

            using (TlvBlock tlv05 = new TlvBlock())
            {
                tlv05.WriteUshort(0x000A, 0x0001);
                tlv05.WriteEmpty(0x000F);
                tlv05.WriteString(0x000C, message, enc);

                ByteStream tlv2711 = new ByteStream();
                tlv2711.WriteUshort(chatroom.Exchange);
                tlv2711.WriteByte(roomnamelength);
                tlv2711.WriteString(chatroom.FullName, enc);
                tlv2711.WriteUshort(chatroom.Instance);

                tlv05.WriteByteArray(0x2711, tlv2711.GetBytes());

                header.WriteByteArray(tlv05.GetBytes());
            }

            stream.WriteUshort(0x0005);
            stream.WriteUshort((ushort) header.GetByteCount());
            stream.WriteByteArray(header.GetBytes());

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));

            return cookie;
        }

        /// <summary>
        /// Sends a direct connection request
        /// </summary>
        /// <param name="conn">A <see cref="DirectConnection"/> object that will handle the request</param>
        public void RequestDirectConnection(DirectConnection conn)
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_OUTGOING_MESSAGE;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            ByteStream stream = new ByteStream();
            InsertIcbmHeader(stream, conn.Cookie, 0x0002, conn.Other.ScreenName);
            using (ByteStream tlv05 = new ByteStream())
            {
                tlv05.WriteUshort(RendezvousData.UshortFromType(RendezvousType.Invite));
                tlv05.WriteByteArray(conn.Cookie.ToByteArray());
                tlv05.WriteByteArray(CapabilityProcessor.GetCapabilityArray(conn.Capability));

                using (TlvBlock tlvs = new TlvBlock())
                {
                    tlvs.WriteUshort(DC_SEQUENCE_NUMBER, RendezvousData.UshortFromSequence(conn.Sequence));
                    if (conn.Sequence == RendezvousSequence.DirectOrStage1)
                    {
                        tlvs.WriteEmpty(0x000F);
                    }
                    if (!String.IsNullOrEmpty(conn.Message))
                    {
                        tlvs.WriteString(DC_MESSAGE, conn.Message, Encoding.ASCII);
                    }

                    uint ipaddress = 0;
                    if (conn.Method == DirectConnectionMethod.Proxied)
                    {
                        ipaddress = ConvertIPAddress(conn.ProxyIP);
                    }
                    else
                    {
                        ipaddress = ConvertIPAddress(conn.ClientIP);
                    }

                    tlvs.WriteUint(DC_PROXY_IP_ADDRESS, ipaddress);
                    tlvs.WriteUint(DC_PROXY_IP_ADDRESS_COMPLIMENT, ~ipaddress);

                    if (conn.Sequence == RendezvousSequence.DirectOrStage1)
                    {
                        tlvs.WriteUint(DC_CLIENT_IP_ADDRESS, ConvertIPAddress(conn.ClientIP));
                    }

                    if (conn.Sequence != RendezvousSequence.Stage3)
                    {
                        tlvs.WriteUshort(DC_PORT, (ushort)conn.Port);
                        tlvs.WriteUshort(DC_PORT_COMPLIMENT, (ushort)(~conn.Port));
                    }

                    if (conn.Method == DirectConnectionMethod.Proxied)
                    {
                        tlvs.WriteEmpty(DC_USE_PROXY_FLAG);
                    }

                    if (conn is FileTransferConnection && conn.Sequence == RendezvousSequence.DirectOrStage1)
                    {
                        FileTransferConnection ftc = conn as FileTransferConnection;
                        using (ByteStream tlv2711 = new ByteStream())
                        {
                            tlv2711.WriteUshort((ushort)((ftc.TotalFiles > 1) ? 0x0002 : 0x0001));
                            tlv2711.WriteUshort((ushort)ftc.TotalFiles);
                            tlv2711.WriteUint(ftc.TotalFileSize);
                            tlv2711.WriteString(ftc.FileHeader.Name, Encoding.ASCII);
                            tlv2711.WriteByte(0x00);
                            tlvs.WriteByteArray(0x2711, tlv2711.GetBytes());
                        }
                        tlvs.WriteString(0x2712, ftc.LocalFileNameEncoding.WebName, Encoding.ASCII);
                    }

                    tlv05.WriteByteArray(tlvs.GetBytes());
                }

                stream.WriteUshort(0x0005);
                stream.WriteUshort((ushort)tlv05.GetByteCount());
                stream.WriteByteArray(tlv05.GetBytes());
            }

            // Acknowledgement request
            stream.WriteUint(0x00030000);

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        /// <summary>
        /// Cancel a direct connection attempt
        /// </summary>
        public void SendDirectConnectionCancellation(DirectConnection conn, string reason)
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_OUTGOING_MESSAGE;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            ByteStream stream = new ByteStream();
            InsertIcbmHeader(stream, conn.Cookie, 0x0002, conn.Other.ScreenName);
            using (ByteStream tlv05 = new ByteStream())
            {
                tlv05.WriteUshort(RendezvousData.UshortFromType(RendezvousType.Cancel));
                tlv05.WriteByteArray(conn.Cookie.ToByteArray());
                tlv05.WriteByteArray(CapabilityProcessor.GetCapabilityArray(conn.Capability));

                using (TlvBlock tlvs = new TlvBlock())
                {
                    tlvs.WriteUshort(0x000B, 0x0001);
                    tlvs.WriteString(0x000C, reason, Encoding.ASCII);
                    tlvs.WriteEmpty(0x0003);
                    tlv05.WriteByteArray(tlvs.GetBytes());
                }

                stream.WriteUshort(0x0005);
                stream.WriteUshort((ushort)tlv05.GetByteCount());
                stream.WriteByteArray(tlv05.GetBytes());
            }

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        /// <summary>
        /// Accept a direct connection attempt
        /// </summary>
        public void SendDirectConnectionAccept(DirectConnection conn)
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_OUTGOING_MESSAGE;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            ByteStream stream = new ByteStream();
            InsertIcbmHeader(stream, conn.Cookie, 0x0002, conn.Other.ScreenName);
            using (ByteStream tlv05 = new ByteStream())
            {
                tlv05.WriteUshort(RendezvousData.UshortFromType(RendezvousType.Accept));
                tlv05.WriteByteArray(conn.Cookie.ToByteArray());
                tlv05.WriteByteArray(CapabilityProcessor.GetCapabilityArray(conn.Capability));
                tlv05.WriteUint(0x00030000);

                stream.WriteUshort(0x0005);
                stream.WriteUshort((ushort)tlv05.GetByteCount());
                stream.WriteByteArray(tlv05.GetBytes());
            }

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        /// <summary>
        /// Sends a warning to a remote client -- SNAC(04,08)
        /// </summary>
        /// <param name="screenname">The screenname of the client to warn</param>
        /// <param name="anonymous">Send the warning as anonymous or as yourself</param>
        public void SendWarning(string screenname, bool anonymous)
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_SEND_WARNING;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            ByteStream stream = new ByteStream();
            stream.WriteUshort((ushort)((anonymous) ? 1 : 0));
            stream.WriteByte((byte)Encoding.ASCII.GetByteCount(screenname));
            stream.WriteString(screenname, Encoding.ASCII);

            // Save the screenname so the handler for the reply SNAC can retrieve it
            parent.StoreRequestID(sh.RequestID, screenname);

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        /// <summary>
        /// Sends a request for parameter information -- SNAC(04,04)
        /// </summary>
        /// <param name="sess">A <see cref="Session"/> object</param>
        internal void RequestParametersList()
        {
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_REQUEST_PARAMETERS;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, new ByteStream()));
        }

        /// <summary>
        /// Sets ICBM parameters to more reasonable values than the defaults -- SNAC(04,02)
        /// </summary>
        /// <param name="sess">A <see cref="Session"/> object</param>
        internal void UpdateParameters()
        {
            // Construct the SNAC header
            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_SET_PARAMETERS;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            // Put the ICBM parameters into a byte array
            ByteStream stream = new ByteStream();
            // Channel to setup -- should always be 0
            stream.WriteUshort(0x0000);
            // Message flags
            stream.WriteUint(0x0000000B);
            // Maximum message size (8192 bytes)
            parent.Limits.MaxMessageSize = 0x1F40;
            stream.WriteUshort(0x1F40);
            // Maximum sender evil level
            parent.Limits.MaxSenderWarningLevel = 0x0384;
            stream.WriteUshort(0x0384);
            // Maximum receiver evil level
            parent.Limits.MaxReceiverWarningLevel = 0x03E7;
            stream.WriteUshort(0x03E7);
            // Minimum message interval, in seconds
            stream.WriteUint(0x00000000);

            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
        }

        #endregion

        #region ISnacFamilyHandler Members

        /// <summary>
        /// Process an incoming <see cref="DataPacket"/> from SNAC family 4
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> received by the server</param>
        public void ProcessIncomingPacket(DataPacket dp)
        {
            switch (dp.SNAC.FamilySubtypeID)
            {
                case ICBM_PARAMETER_LIST:
                    ProcessParametersList(dp);
                    break;
                case ICBM_INCOMING_MESSAGE:
                    ProcessMessage(dp);
                    break;
                case ICBM_WARNING_ACKNOWLEDGED:
                    ProcessWarningAcknowledgement(dp);
                    break;
                case ICBM_UNDELIVERABLE_MESSAGE:
                    ProcessUndeliverableMessage(dp);
                    break;
                case ICBM_MESSAGE_DELIVERED:
                    ProcessServerAcknowledgement(dp);
                    break;
                case ICBM_TYPING_NOTIFICATION:
                    ProcessTypingNotification(dp);
                    break;
            }
        }

        #endregion

        /// <summary>
        /// Raised when a new message is received
        /// </summary>
        public event MessageReceivedHandler MessageReceived;

        /// <summary>
        /// Occurs when the server sends an undeliverable message notification
        /// </summary>
        /// <remarks>
        /// This event is raised when a remote client has attempted to send a message
        /// to your client, but it could not be delivered
        /// </remarks>
        public event UndeliverableMessageEventHandler MessageUndeliverable;

        /// <summary>
        /// Occurs when the server sends acknowledgement that a message was accepted for delivery
        /// </summary>
        public event MessageAcceptedEventHandler MessageAccepted;

        /// <summary>
        /// Occurs when the server sends a typing notification
        /// </summary>
        public event TypingNotificationEventHandler TypingNotification;

        /// <summary>
        /// Sends an ICBM on channel 1 -- SNAC(04,06)
        /// </summary>
        /// <param name="destination">The screenname to receive the IM</param>
        /// <param name="message">The message to send</param>
        /// <param name="flags">A <see cref="MessageFlags"/> enumeration specifying what
        /// additional information should be sent with the message</param>
        private Cookie SendMessageThroughServer(string destination, string message, MessageFlags flags)
        {
            Console.WriteLine("Sending message through server");

            SNACHeader sh = new SNACHeader();
            sh.FamilyServiceID = SNAC_ICBM_FAMILY;
            sh.FamilySubtypeID = ICBM_OUTGOING_MESSAGE;
            sh.Flags = 0x0000;
            sh.RequestID = Session.GetNextRequestID();

            byte[] default_features = new byte[] {0x01, 0x01, 0x01, 0x02};
            Cookie cookie = Cookie.CreateCookieForSending();
            Encoding encoding = UtilityMethods.FindBestOscarEncoding(message);

            ByteStream stream = new ByteStream();

            InsertIcbmHeader(stream, cookie, 0x0001, destination);
            using (TlvBlock tlvs = new TlvBlock())
            {
                // Write in TLV 0x0002: a text message
                using (TlvBlock tlv02 = new TlvBlock())
                {
                    tlv02.WriteByteArray(0x0501, default_features);
                    ByteStream feature0101 = new ByteStream();
                    feature0101.WriteUshort(Marshal.EncodingToCharset(encoding));
                    feature0101.WriteUshort(0x0000);
                    feature0101.WriteString(message, encoding);
                    tlv02.WriteByteArray(0x0101, feature0101.GetBytes());
                    tlvs.WriteByteArray(0x0002, tlv02.GetBytes());
                }

                // Pack in optional TLVs
                if ((flags & MessageFlags.AutoResponse) != 0)
                {
                    tlvs.WriteEmpty(0x0004);
                }
                if ((flags & MessageFlags.RequestAcknowledgement) != 0)
                {
                    tlvs.WriteEmpty(0x0003);
                }
                if ((flags & MessageFlags.HasIcon) != 0)
                {
                    ByteStream iconStream = new ByteStream();
                    iconStream.WriteUint(parent.SSI.IconItem.IconSize);
                    iconStream.WriteUshort(0x0001);
                    iconStream.WriteUshort(parent.SSI.IconItem.Checksum);
                    iconStream.WriteUint(parent.SSI.IconItem.Stamp);

                    tlvs.WriteByteArray(0x0008, iconStream.GetBytes());
                }
                if ((flags & MessageFlags.RequestIcon) != 0)
                {
                    tlvs.WriteEmpty(0x0009);
                }

                // Add the TLVs to the byte stream
                stream.WriteByteArray(tlvs.GetBytes());
            }

            // Send that sucker off
            SNACFunctions.BuildFLAP(Marshal.BuildDataPacket(parent, sh, stream));
            return cookie;
        }

        /// <summary>
        /// Inserts an ICBM header
        /// </summary>
        private static void InsertIcbmHeader(ByteStream stream, Cookie cookie, ushort channel, string screenName)
        {
            stream.WriteByteArray(cookie.ToByteArray());
            stream.WriteUshort(channel);
            stream.WriteByte((byte) Encoding.ASCII.GetByteCount(screenName));
            stream.WriteString(screenName, Encoding.ASCII);
        }

        /// <summary>
        /// Converts an IP address string to a NBO uint
        /// </summary>
        private static uint ConvertIPAddress(string ipAddress)
        {
            string[] IPsplit = ipAddress.Split('.');
            byte shift = 24;
            uint complement = 0x00000000;
            // Convert the IP into an NBO uint
            for (int i = 0; i < 4; i++)
            {
                byte b = Byte.Parse(IPsplit[i]);
                complement |= (uint)(b << shift);
                shift -= 8;
            }
            return complement;
        }

        #region Handlers

        /// <summary>
        /// Processes the parameter information sent by the server -- SNAC(04,05)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,05)</param>
        private void ProcessParametersList(DataPacket dp)
        {
            ushort max_channel = dp.Data.ReadUshort();
            uint flags = dp.Data.ReadUint();
            ushort snac_size = dp.Data.ReadUshort();
            ushort sender_evil = dp.Data.ReadUshort();
            ushort receiver_evil = dp.Data.ReadUshort();
            ushort message_interval = dp.Data.ReadUshort();
            ushort unknown_data = dp.Data.ReadUshort();

            // TODO:  Do something with these capabilities?
            parent.ParameterSetArrived();

            // Send SNAC(04,02) to rid ourselves of these foolish default settings
            UpdateParameters();
        }

        /// <summary>
        /// Processes the acknowledgement of a warning sent by the server -- SNAC(04,09)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,09)</param>
        private void ProcessWarningAcknowledgement(DataPacket dp)
        {
            ushort warning_level_gain = dp.Data.ReadUshort();
            ushort new_warning_level = dp.Data.ReadUshort();
            // TODO:  We don't really do anything with this
            //parent.OnWarningAcknowledgement(dp.SNAC.RequestID, warning_level_gain, new_warning_level);
        }

        /// <summary>
        /// Processes an undeliverable message notification sent by the server -- SNAC(04,0A)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,0A)</param>
        private void ProcessUndeliverableMessage(DataPacket dp)
        {
            while (dp.Data.HasMoreData)
            {
                ushort messagechannel = dp.Data.ReadUshort();
                UserInfo ui = dp.Data.ReadUserInfo();
                ushort numbermissed = dp.Data.ReadUshort();
                UndeliverableMessageReason reason = (UndeliverableMessageReason) dp.Data.ReadUshort();

                if (MessageUndeliverable != null)
                {
                    MessageUndeliverable(this,
                                         new UndeliverableMessageEventArgs(messagechannel, ui, numbermissed, reason));
                }
            }
        }

        /// <summary>
        /// Processes an "message accepted" notification sent by the server -- SNAC(04,0C)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,0C)</param>
        private void ProcessServerAcknowledgement(DataPacket dp)
        {
            Cookie cookie = Cookie.GetReceivedCookie(dp.Data.ReadByteArray(8));
            ushort messagechannel = dp.Data.ReadUshort();
            string screenname = dp.Data.ReadString(dp.Data.ReadByte(), Encoding.ASCII);

            if (MessageAccepted != null)
            {
                MessageAccepted(this, new MessageAcceptedEventArgs(cookie, screenname));
            }
        }

        /// <summary>
        /// Processes a typing notification sent by the server -- SNAC(04,14)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,14)</param>
        private void ProcessTypingNotification(DataPacket dp)
        {
            byte[] cookie = dp.Data.ReadByteArray(8);
            ushort notification_channel = dp.Data.ReadUshort();
            string screenname = dp.Data.ReadString(dp.Data.ReadByte(), Encoding.ASCII);
            TypingNotification tn = (TypingNotification) dp.Data.ReadUshort();

            if (TypingNotification != null)
            {
                TypingNotification(this, new TypingNotificationEventArgs(screenname, tn));
            }
        }

        #region SNAC(04,07) -- Receive ICBM

        /// <summary>
        /// Processes an ICBM message sent by the server -- SNAC(04,07)
        /// </summary>
        /// <param name="dp">A <see cref="DataPacket"/> object with a buffer containing SNAC(04,07)</param>
        private void ProcessMessage(DataPacket dp)
        {
            // Pull apart the fixed part of the message
            byte[] message_cookie = dp.Data.ReadByteArray(8);
            ushort channel = dp.Data.ReadUshort();
            UserInfo ui = dp.Data.ReadUserInfo();

            IM message = new IM(ui);
            message.Cookie = Cookie.GetReceivedCookie(message_cookie);

            // End of the fixed part. Pull apart the channel-specific data
            switch (channel)
            {
                case 0x0001:
                    ProcessChannelOneMessage(dp.Data, ui);
                    break;
                case 0x0002:
                    ProcessChannelTwoMessage(dp.Data, ui);
                    break;
                default:
                    parent.OnWarning(ServerErrorCode.UnknownMessageChannel);
                    break;
            }
        }

        #region SNAC(04,07):01

        /// <summary>
        /// Processes an incoming ICBM message on channel 1 -- SNAC(04,07)
        /// </summary>
        /// <param name="stream">A received <see cref="ByteStream"/></param>
        /// <param name="ui">The UserInfo block that came with this message</param>
        private void ProcessChannelOneMessage(ByteStream stream, UserInfo ui)
        {
            IM message = new IM(ui);
            using (TlvBlock tlvs = new TlvBlock(stream.ReadByteArrayToEnd()))
            {
                message.IsAutoResponse = tlvs.HasTlv(0x0004);
                // If this message was received offline, cast it to an OfflineIM
                if (tlvs.HasTlv(0x0006))
                {
                    message = new OfflineIM(message);
                    if (tlvs.HasTlv(0x0016))
                    {
                        ((OfflineIM)message).ReceivedOn = tlvs.ReadDateTime(0x0016);
                    }
                }
                GetChannelOneMessage(new ByteStream(tlvs.ReadByteArray(0x0002)), ref message);
            }

            // Figure out what to do with it. There's some hoo-hah around ICQ's offline
            // message system sometimes sending the message via the IcqManager and
            // sometimes via the MessageManager. It might depend on the encoding of the message.
            if (parent.ICQ.IsReceivingOfflineMessages)
            {
                // Determine if this is really meant for the ICQ manager
                // Check if we're signed in via ICQ, since I hear AIM supports OIMs now
                if (ScreennameVerifier.IsValidICQ(parent.ScreenName) && message is OfflineIM)
                {
                    parent.ICQ.AcceptIcbmOIM(message as OfflineIM);
                    return;
                }
            }

            OnMessageReceived(message);
        }

        /// <summary>
        /// Raises the <see cref="MessageReceived"/> event
        /// </summary>
        /// <param name="message">The <see cref="IM"/> that was received</param>
        protected internal void OnMessageReceived(IM message)
        {
            // Check to see if there is a DirectIM connection that has been superceded by this message
            DirectIMConnection conn = parent.Connections.GetDirectIMByScreenname(message.ScreenName);
            if (conn != null && !conn.Connected)
            {
                parent.Connections.RemoveDirectConnection(conn.Cookie);
                parent.OnDirectIMSessionClosed(conn.Other, conn.Cookie);
                conn = null;
            }

            // Alert the user of the message
            if (MessageReceived != null)
            {
                MessageReceived(this, new MessageReceivedEventArgs(message));
            }
        }

        /// <summary>
        /// Retrieves the message text from SNAC(04,07) TLV 02
        /// </summary>
        /// <param name="stream">A received <see cref="ByteStream"/></param>
        /// <param name="message">An <see cref="IM"/> object to be populated</param>
        private void GetChannelOneMessage(ByteStream stream, ref IM message)
        {
            while (stream.HasMoreData)
            {
                byte identifier = stream.ReadByte();
                byte version = stream.ReadByte();
                ushort length = stream.ReadUshort();
                switch (identifier)
                {
                    case 0x01: // Message text
                        ushort charset = stream.ReadUshort();
                        ushort charsubset = stream.ReadUshort();
                        Encoding incoming = IM.GetEncodingFromCharset(charset, charsubset);
                        message.Message = Encoding.Unicode.GetString(
                            Encoding.Convert(incoming, Encoding.Unicode, stream.ReadByteArray(length - 4)));

                        break;
                    default: // Unhandled case
                        stream.AdvanceOffset(length);
                        break;
                }
            }
        }

        #endregion

        #region SNAC(04,07):02

        /// <summary>
        /// Process a ICBM that was sent over channel two - rich text messages, chat/filetransfer invites, buddy icons
        /// </summary>
        private void ProcessChannelTwoMessage(ByteStream stream, UserInfo ui)
        {
            TlvBlock tlvs = new TlvBlock(stream.ReadByteArrayToEnd());
            ushort errorCode = tlvs.ReadUshort(0x000B);
            DirectConnection conn = ProcessChannel2Tlv05(new ByteStream(tlvs.ReadByteArray(0x0005)), ui);
        }

        /// <summary>
        /// Processes the inner TLV list in TLV 0x0005 and returns a new DirectConnection
        /// </summary>
        private DirectConnection ProcessChannel2Tlv05(ByteStream stream, UserInfo ui)
        {
            byte[] invitemessage;
            Encoding encoding = Encoding.ASCII;
            string language = "en";

            DirectConnection directconn = null;

            // Pull the type, cookie, and capability array
            RendezvousType rtype = RendezvousData.TypeFromUshort(stream.ReadUshort());
            Cookie cookie = Cookie.GetReceivedCookie(stream.ReadByteArray(8));
            byte[] capabilitiesArray = stream.ReadByteArray(16);
            Capabilities capabilities = CapabilityProcessor.ProcessCLSIDList(capabilitiesArray);

            // Create the correct type of connection based on the capability
            if (capabilities == Capabilities.SendFiles)
            {
                if ((directconn = parent.Connections.GetDirectConnectionByCookie(cookie)) == null)
                {
                    directconn = parent.Connections.CreateNewFileTransferConnection(
                        DirectConnectionMethod.Direct, DirectConnectRole.Receiver);
                }
            }
            else if (capabilities == Capabilities.DirectIM)
            {
                if ((directconn = parent.Connections.GetDirectConnectionByCookie(cookie)) == null)
                {
                    directconn = parent.Connections.CreateNewDirectIMConnection(
                        DirectConnectionMethod.Direct, DirectConnectRole.Receiver);
                }
            }
            else if (capabilities == Capabilities.Chat)
            {
                directconn = parent.Connections.CreateNewChatInvitationConnection(DirectConnectRole.Receiver);
            }
            else
            {
                // Currently unsupported
                parent.OnWarning(ServerErrorCode.UnknownRendezvousChannel);
                return null;
            }

            directconn.Other = ui;
            directconn.Cookie = cookie;
            directconn.Type = rtype;

            ByteStream serviceData = null;
            Encoding serviceDataEncoding = Encoding.ASCII;

            // Process the inner TLV list
            using (TlvBlock tlvs = new TlvBlock(stream.ReadByteArrayToEnd()))
            {
                directconn.ProxyIP = tlvs.ReadIPAddress(DC_PROXY_IP_ADDRESS);
                directconn.ClientIP = tlvs.ReadIPAddress(DC_CLIENT_IP_ADDRESS);
                directconn.VerifiedIP = tlvs.ReadIPAddress(0x0004);
                directconn.Port = tlvs.ReadUshort(DC_PORT);
                directconn.Sequence = RendezvousData.SequenceFromUshort(tlvs.ReadUshort(DC_SEQUENCE_NUMBER));
                invitemessage = tlvs.ReadByteArray(DC_MESSAGE);
                if (tlvs.HasTlv(0x000D))
                {
                    encoding = Encoding.GetEncoding(tlvs.ReadString(0x000D, Encoding.ASCII));
                }
                language = tlvs.ReadString(0x000E, Encoding.ASCII);
                directconn.Method = (tlvs.HasTlv(DC_USE_PROXY_FLAG))
                                        ?
                                            DirectConnectionMethod.Proxied
                                        : DirectConnectionMethod.Direct;

                serviceData = new ByteStream(tlvs.ReadByteArray(0x2711));
                if (tlvs.HasTlv(0x2712))
                {
                    serviceDataEncoding = Encoding.GetEncoding(tlvs.ReadString(0x2712, Encoding.ASCII));
                }
            }

            if (invitemessage != null)
            {
                directconn.Message = encoding.GetString(invitemessage, 0, invitemessage.Length);
            }

            // Process the extra data, if necessary
            if (directconn is FileTransferConnection || directconn is DirectIMConnection)
            {
                ProcessDirectConnectionRequest(directconn, serviceData);
            }
            else if (directconn is ChatInvitationConnection)
            {
                ChatInvitationConnection cic = directconn as ChatInvitationConnection;
                cic.ChatInvite = new ChatInvitation();
                cic.ChatInvite.Message = directconn.Message;
                cic.ChatInvite.Encoding = encoding;
                cic.ChatInvite.Language = language;
                ProcessChatInvitationRequest(cic, serviceData);
            }

            return directconn;
        }

        /// <summary>
        /// Performs processing on the 0x2711 TLV of a chat invitation request
        /// </summary>
        private void ProcessChatInvitationRequest(ChatInvitationConnection conn, ByteStream serviceData)
        {
            if (conn.Type == RendezvousType.Accept)
            {
                // Accepting chat invitation. Does this ever get received?
            }
            else if (conn.Type == RendezvousType.Cancel)
            {
                // Cancelling chat invitation. Jerks.
            }
            else if (conn.Type == RendezvousType.Invite && serviceData.HasMoreData)
            {
                conn.ChatRoom = new ChatRoom(serviceData);
                parent.ChatRooms.CacheChatRoomInvitation(conn.Cookie, conn.ChatRoom);

                parent.OnChatInvitationReceived(conn.Other, conn.ChatRoom.DisplayName,
                                                conn.ChatInvite.Message, conn.ChatInvite.Encoding,
                                                conn.ChatInvite.Language,
                                                conn.Cookie);
            }
        }

        /// <summary>
        /// Performs TLV 0x2711 processing for direct connect (sendfiles, DirectIM) negotiation
        /// </summary>
        private void ProcessDirectConnectionRequest(DirectConnection conn, ByteStream stream)
        {
            if (conn.Type == RendezvousType.Accept)
            {
                // They're accepting, which means we're going to get a connection on the
                // listener socket set up in FileTransferManager. Do nothing here
            }
            else if (conn.Type == RendezvousType.Cancel)
            {
                DirectConnection cancelled = parent.Connections.GetDirectConnectionByCookie(conn.Cookie);

                if (cancelled != null)
                {
                    if (cancelled is FileTransferConnection)
                    {
                        (cancelled as FileTransferConnection).CancelFileTransfer(
                            "Remote user cancelled direct connection");
                    }
                    else if (cancelled is DirectIMConnection)
                    {
                        parent.OnDirectIMSessionCancelled(cancelled, "Remote user cancelled direct connection");
                    }
                }

                return;
            }
            else if (conn.Type == RendezvousType.Invite)
            {
                // AIM sends a type 0x0000 when accepting...huh.

                if (conn.Sequence == RendezvousSequence.DirectOrStage1)
                {
                    if (stream.HasMoreData && conn is FileTransferConnection)
                    {
                        FileTransferConnection ftconn = conn as FileTransferConnection;
                        ftconn.SubType = stream.ReadUshort();
                        ftconn.TotalFiles = stream.ReadUshort();
                        ftconn.TotalFileSize = stream.ReadUint();

                        int strlen = 0;
                        byte[] servicedata = stream.ReadByteArrayToEnd();
                        // The filename in an RD invite is null-terminated ASCII
                        while (servicedata[strlen++] != 0x00) ;
                        ftconn.FileHeader.Name = Encoding.ASCII.GetString(servicedata, 0, strlen - 1);
                    }

                    parent.OnDirectConnectionRequestReceived(conn.Cookie);
                }
                else if (conn.Sequence == RendezvousSequence.Stage2)
                {
                    // The receipient of a previous invite wants a stage 2 proxy redirection
                    // Shut down the server socket, we won't be getting a connection on it
                    conn.StopListeningSocket();
                    SendDirectConnectionAccept(conn);
                    conn.StartSendThroughStage2Proxy();
                }
                else if (conn.Sequence == RendezvousSequence.Stage3)
                {
                    // Direct connection and proxy 2 failed, the sender's trying to proxy it now
                    conn.Method = DirectConnectionMethod.Proxied;
                    conn.ConnectToServer();
                }
            }
        }

        // End region SNAC(04,07):02

        #endregion

        // End region SNAC(04,07)

        #endregion

        #endregion
    }
}
